Avastage JavaScripti objektorienteeritud programmeerimise arengut. Põhjalik juhend prototüübilisest pärilusest, konstruktorimustritest, ES6 klassidest ja kompositsioonist.
JavaScripti päriluse meisterlik valdamine: sügavuti klassimustritesse
Objektorienteeritud programmeerimine (OOP) on paradigma, mis on kujundanud kaasaegset tarkvaraarendust. Oma olemuselt võimaldab OOP meil modelleerida reaalse maailma olemusi objektidena, ühendades andmed (omadused) ja käitumise (meetodid). Üks võimsamaid kontseptsioone OOP-s on pärilus – mehhanism, mille abil üks objekt või klass saab omandada teise omadused ja meetodid. JavaScripti maailmas on pärilusel ainulaadne ja põnev ajalugu, arenedes puhtalt prototüübilisest mudelist tänapäevaseks tuttavamaks klassipõhiseks süntaksiks. Globaalsele arendajaskonnale ei ole nende mustrite mõistmine pelgalt akadeemiline harjutus; see on praktiline vajadus puhta, korduvkasutatava ja skaleeritava koodi kirjutamiseks.
See põhjalik juhend viib teid rännakule läbi JavaScripti päriluse maastiku. Alustame fundamentaalsest prototüübiahelast, uurime aastaid domineerinud klassikalisi mustreid, demüstifitseerime kaasaegse ES6 `class` süntaksi ja lõpuks vaatame võimsaid alternatiive nagu kompositsioon. Olenemata sellest, kas olete algaja arendaja, kes püüab põhitõdesid haarata, või kogenud professionaal, kes soovib oma teadmisi kinnistada, pakub see artikkel teile vajalikku selgust ja sügavust.
Alus: JavaScripti prototüübilise olemuse mõistmine
Enne kui saame rääkida klassidest või pärilusmustritest, peame mõistma fundamentaalset mehhanismi, mis seda kõike JavaScriptis toidab: prototüübiline pärilus. Erinevalt keeltest nagu Java või C++, ei ole JavaScriptil klasse traditsioonilises mõttes. Selle asemel pärivad objektid otse teistelt objektidelt. Igal JavaScripti objektil on privaatne omadus, mida sageli tähistatakse kui `[[Prototype]]`, mis on link teisele objektile. Seda teist objekti nimetatakse tema prototüübiks.
Mis on prototĂĽĂĽp?
Kui proovite pääseda ligi objekti omadusele, kontrollib JavaScripti mootor esmalt, kas omadus eksisteerib objektil endal. Kui mitte, vaatab see objekti prototüüpi. Kui seda sealt ei leita, vaatab see prototüübi prototüüpi ja nii edasi. Seda seotud prototüüpide seeriat tuntakse kui prototüübiahelat. Ahel lõpeb, kui see jõuab prototüübini, mis on `null`.
Vaatame lihtsat näidet:
// Loome mustandobjekti
const animal = {
breathes: true,
speak() {
console.log("This animal makes a sound.");
}
};
// Loome uue objekti, mis pärib 'animal' objektist
const dog = Object.create(animal);
dog.name = "Buddy";
console.log(dog.name); // Väljund: Buddy (leitud 'dog' objektilt endalt)
console.log(dog.breathes); // Väljund: true (ei ole 'dog' peal, leitud tema prototüübilt 'animal')
dog.speak(); // Väljund: This animal makes a sound. (leitud 'animal' pealt)
console.log(Object.getPrototypeOf(dog) === animal); // Väljund: true
Selles näites pärib `dog` objektilt `animal`. Kui me kutsume välja `dog.breathes`, ei leia JavaScript seda `dog` pealt, seega järgib see `[[Prototype]]` linki `animal` objektini ja leiab selle sealt. See on prototüübiline pärilus oma kõige puhtamal kujul.
PrototĂĽĂĽbiahel tegevuses
Mõelge prototüübiahelast kui omaduste otsimise hierarhiast:
- Objekti tase: `dog` omab `name`.
- PrototĂĽĂĽbi tase 1: `animal` (`dog` prototĂĽĂĽp) omab `breathes` ja `speak`.
- PrototĂĽĂĽbi tase 2: `Object.prototype` (`animal` prototĂĽĂĽp, kuna see loodi literaalina) omab meetodeid nagu `toString()` ja `hasOwnProperty()`.
- Ahela lõpp: `Object.prototype` prototüüp on `null`.
See ahel on kõigi JavaScripti pärilusmustrite aluskivi. Isegi kaasaegne `class` süntaks on, nagu näeme, süntaktiline suhkur, mis on ehitatud just selle süsteemi peale.
Klassikalised pärilusmustrid ES6-eelses JavaScriptis
Enne `class` võtmesõna kasutuselevõttu ES6-s (ECMAScript 2015) mõtlesid arendajad välja mitmeid mustreid, et jäljendada teistes keeltes leiduvat klassikalist pärilust. Nende mustrite mõistmine on ülioluline vanemate koodibaasidega töötamisel ja selle hindamisel, mida ES6 klassid lihtsustavad.
Muster 1: Konstruktor funktsioonid
See oli kõige levinum viis objektide "mustandite" loomiseks. Konstruktor funktsioon on lihtsalt tavaline funktsioon, kuid seda kutsutakse välja `new` võtmesõnaga.
Kui funktsioon kutsutakse välja `new`-ga, juhtub neli asja:
- Luuakse uus tĂĽhi objekt ja see seotakse funktsiooni `prototype` omadusega.
- `this` võtmesõna funktsiooni sees seotakse selle uue objektiga.
- Funktsiooni kood käivitatakse.
- Kui funktsioon ei tagasta selgesõnaliselt objekti, tagastatakse sammus 1 loodud uus objekt.
function Vehicle(make, model) {
// Eksemplari omadused - igale objektile unikaalsed
this.make = make;
this.model = model;
}
// Jagatud meetodid - eksisteerivad prototüübil mälu säästmiseks
Vehicle.prototype.getDetails = function() {
return `${this.make} ${this.model}`;
};
const car1 = new Vehicle("Toyota", "Camry");
const car2 = new Vehicle("Honda", "Civic");
console.log(car1.getDetails()); // Väljund: Toyota Camry
console.log(car2.getDetails()); // Väljund: Honda Civic
// Mõlemad eksemplarid jagavad sama getDetails funktsiooni
console.log(car1.getDetails === car2.getDetails); // Väljund: true
See muster töötab hästi objektide loomiseks malli põhjal, kuid ei tegele iseenesest pärilusega. Selle saavutamiseks kombineerisid arendajad seda teiste tehnikatega.
Muster 2: Kombineeritud pärilus (klassikaline muster)
See oli aastaid eelistatud muster. See kombineerib kahte tehnikat:
- Konstruktori varastamine: Kasutades `.call()` või `.apply()`, et käivitada vanema konstruktor lapse kontekstis. See pärib kõik eksemplari omadused.
- Prototüübiaheldus: Seades lapse prototüübi vanema eksemplariks. See pärib kõik jagatud meetodid.
Loome `Car` (Auto), mis pärib `Vehicle` (Sõiduk) klassist.
// Vanema konstruktor
function Vehicle(make, model) {
this.make = make;
this.model = model;
}
Vehicle.prototype.getDetails = function() {
return `${this.make} ${this.model}`;
};
// Lapse konstruktor
function Car(make, model, numDoors) {
// 1. Konstruktori varastamine: Päri eksemplari omadused
Vehicle.call(this, make, model);
this.numDoors = numDoors;
}
// 2. Prototüübiaheldus: Päri jagatud meetodid
Car.prototype = Object.create(Vehicle.prototype);
// 3. Paranda konstruktori omadus
Car.prototype.constructor = Car;
// Lisa meetod, mis on spetsiifiline Autole
Car.prototype.honk = function() {
console.log("Beep beep!");
};
const myCar = new Car("Ford", "Focus", 4);
console.log(myCar.getDetails()); // Väljund: Ford Focus (Päritud Vehicle.prototype-ist)
console.log(myCar.numDoors); // Väljund: 4
myCar.honk(); // Väljund: Beep beep!
console.log(myCar instanceof Car); // Väljund: true
console.log(myCar instanceof Vehicle); // Väljund: true
Plussid: See muster on robustne. See eraldab korrektselt eksemplari omadused jagatud meetoditest ja säilitab prototüübiahela `instanceof` kontrollide jaoks.
Miinused: See on veidi paljusõnaline ja nõuab prototüübi ning konstruktori omaduse käsitsi ühendamist. Nimi "Kombineeritud pärilus" viitab mõnikord veidi vähem optimaalsele versioonile, kus kasutatakse `Car.prototype = new Vehicle()`, mis kutsub asjatult `Vehicle` konstruktori kaks korda välja. Ülaltoodud `Object.create()` meetod on optimeeritud lähenemine, mida sageli nimetatakse parasiitseks kombineeritud päriluseks.
Kaasaegne ajastu: ES6 klasside pärilus
ECMAScript 2015 (ES6) tõi kaasa uue süntaksi objektide loomiseks ja päriluse haldamiseks. `class` ja `extends` võtmesõnad pakuvad palju puhtamat ja tuttavamat süntaksit arendajatele, kes tulevad teistest OOP-keeltest. Siiski on ülioluline meeles pidada, et see on süntaktiline suhkur JavaScripti olemasoleva prototüübilise päriluse peal. See ei too kaasa uut objektimudelit.
`class` ja `extends` võtmesõnad
Refaktoreerime oma `Vehicle` ja `Car` näite, kasutades ES6 klasse. Tulemus on dramaatiliselt puhtam.
// Vanemklass
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}
getDetails() {
return `${this.make} ${this.model}`;
}
}
// Lapsklass
class Car extends Vehicle {
constructor(make, model, numDoors) {
// Kutsu vanema konstruktor välja super()-ga
super(make, model);
this.numDoors = numDoors;
}
honk() {
console.log("Beep beep!");
}
}
const myCar = new Car("Tesla", "Model 3", 4);
console.log(myCar.getDetails()); // Väljund: Tesla Model 3
myCar.honk(); // Väljund: Beep beep!
console.log(myCar instanceof Car); // Väljund: true
console.log(myCar instanceof Vehicle); // Väljund: true
`super()` meetod
`super` võtmesõna on oluline täiendus. Seda saab kasutada kahel viisil:
- Funktsioonina `super()`: Kui seda kutsutakse lapsklassi konstruktoris, kutsub see vanemklassi konstruktori. Peate kutsuma `super()` lapskonstruktoris enne, kui saate kasutada `this` võtmesõna. See on sellepärast, et vanema konstruktor vastutab `this` konteksti loomise ja initsialiseerimise eest.
- Objektina `super.methodName()`: Seda saab kasutada vanemklassi meetodite kutsumiseks. See on kasulik käitumise laiendamiseks, mitte selle täielikuks ülekirjutamiseks.
class Employee {
constructor(name) {
this.name = name;
}
getGreeting() {
return `Hello, my name is ${this.name}.`;
}
}
class Manager extends Employee {
constructor(name, department) {
super(name); // Kutsu vanema konstruktor
this.department = department;
}
getGreeting() {
// Kutsu vanema meetod ja laienda seda
const baseGreeting = super.getGreeting();
return `${baseGreeting} I manage the ${this.department} department.`;
}
}
const manager = new Manager("Jane Doe", "Technology");
console.log(manager.getGreeting());
// Väljund: Hello, my name is Jane Doe. I manage the Technology department.
Kapoti all: Klassid on "erilised funktsioonid"
Kui kontrollite klassi `typeof`-i, näete, et see on funktsioon.
class MyClass {}
console.log(typeof MyClass); // Väljund: "function"
`class` süntaks teeb meie eest automaatselt mõned asjad, mida pidime varem käsitsi tegema:
- Klassi keha käivitatakse "strict mode"-is (ranges režiimis).
- Klassi meetodid ei ole loendatavad (non-enumerable).
- Klasse tuleb välja kutsuda `new`-ga; nende kutsumine tavalise funktsioonina viskab vea.
- `extends` võtmesõna hoolitseb prototüübiahela seadistamise eest (`Object.create()`) ja teeb `super` kättesaadavaks.
See süntaktiline suhkur muudab koodi palju loetavamaks ja vähem veaohtlikuks, abstraheerides ära prototüübi manipuleerimise rutiinsed osad.
Staatilised meetodid ja omadused
Klassid pakuvad ka puhast viisi `static` liikmete defineerimiseks. Need on meetodid ja omadused, mis kuuluvad klassile endale, mitte ühelegi klassi eksemplarile. Need on kasulikud abifunktsioonide loomiseks või klassiga seotud konstantide hoidmiseks.
class TemperatureConverter {
// Staatiline omadus
static ABSOLUTE_ZERO_CELSIUS = -273.15;
// Staatiline meetod
static celsiusToFahrenheit(celsius) {
return (celsius * 9/5) + 32;
}
static fahrenheitToCelsius(fahrenheit) {
return (fahrenheit - 32) * 5/9;
}
}
// Staatilisi liikmeid kutsutakse otse klassi pealt
console.log(`The boiling point of water is ${TemperatureConverter.celsiusToFahrenheit(100)}°F.`);
// Väljund: The boiling point of water is 212°F.
const converterInstance = new TemperatureConverter();
// converterInstance.celsiusToFahrenheit(100); // See viskaks TypeError vea
Klassikalisest pärilusest edasi: Kompositsioon ja mixin'id
Kuigi klassipõhine pärilus on võimas, ei ole see alati parim lahendus. Liigne pärilusele tuginemine võib viia sügavate, jäikade hierarhiateni, mida on raske muuta. Seda nimetatakse sageli "gorilla/banaani probleemiks": sa tahtsid banaani, aga said gorilla, kes hoidis banaani ja kogu džunglit endaga kaasas. Kaks võimast alternatiivi kaasaegses JavaScriptis on kompositsioon ja mixin'id.
Kompositsioon päriluse asemel: "Omab" suhe
"Kompositsioon päriluse asemel" põhimõte soovitab, et peaksite eelistama objektide koostamist väiksematest, iseseisvatest osadest, selle asemel et pärida suurest, monoliitsest baasklassist. Pärilus defineerib "on" suhte (`Auto` on `Sõiduk`). Kompositsioon defineerib "omab" suhte (`Auto` omab `Mootorit`).
Modelleerime erinevat tüüpi roboteid. Sügav pärilusahel võiks välja näha nii: `Robot -> LendavRobot -> LaseritegaRobot`.
See muutub hapraks. Mis siis, kui soovite kõndivat robotit laseritega? Või lendavat robotit ilma nendeta? Kompositsiooniline lähenemine on paindlikum.
// Defineeri võimekused funktsioonidena (tehastena)
const canFly = (state) => ({
fly: () => console.log(`${state.name} is flying!`)
});
const canShootLasers = (state) => ({
shoot: () => console.log(`${state.name} is shooting lasers!`)
});
const canWalk = (state) => ({
walk: () => console.log(`${state.name} is walking.`)
});
// Loo robot, komponeerides võimekusi
const createFlyingLaserRobot = (name) => {
let state = { name };
return Object.assign(
{},
state,
canFly(state),
canShootLasers(state)
);
};
const createWalkingRobot = (name) => {
let state = { name };
return Object.assign(
{},
state,
canWalk(state)
);
}
const robot1 = createFlyingLaserRobot("T-8000");
robot1.fly(); // Väljund: T-8000 is flying!
robot1.shoot(); // Väljund: T-8000 is shooting lasers!
const robot2 = createWalkingRobot("C-3PO");
robot2.walk(); // Väljund: C-3PO is walking.
See muster on uskumatult paindlik. Saate käitumisi vastavalt vajadusele segada ja sobitada, ilma et teid piiraks jäik klassihierarhia.
Mixin'id: funktsionaalsuse laiendamine
Mixin on objekt või funktsioon, mis pakub meetodeid, mida teised klassid saavad kasutada, ilma et see oleks nende klasside vanem. See on viis funktsionaalsuse "sisse segamiseks". See on kompositsiooni vorm, mida saab kasutada isegi ES6 klassidega.
Loome `withLogging` mixin'i, mida saab rakendada mis tahes klassile.
// Mixin
const withLogging = {
log(message) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`)
},
logError(message) {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`)
}
};
class DatabaseService {
constructor(connectionString) {
this.connectionString = connectionString;
}
connect() {
this.log(`Connecting to ${this.connectionString}...`);
// ... ĂĽhenduse loogika
this.log("Connection successful.");
}
}
// Kasuta Object.assign-i, et segada funktsionaalsus klassi prototĂĽĂĽpi
Object.assign(DatabaseService.prototype, withLogging);
const db = new DatabaseService("mongodb://localhost/mydb");
db.connect();
// [LOG] 2023-10-27T10:00:00.000Z: Connecting to mongodb://localhost/mydb...
// [LOG] 2023-10-27T10:00:00.000Z: Connection successful.
db.logError("Failed to fetch user data.");
// [ERROR] 2023-10-27T10:00:00.000Z: Failed to fetch user data.
See lähenemine võimaldab teil jagada ühist funktsionaalsust, nagu logimine, serialiseerimine või sündmuste käsitlemine, omavahel mitteseotud klasside vahel, sundimata neid pärilussuhtesse.
Õige mustri valimine: praktiline juhend
Nii paljude valikute puhul, kuidas otsustada, millist mustrit kasutada? Siin on lihtne juhend globaalsetele arendusmeeskondadele:
-
Kasutage ES6 klasse (`extends`) selgete "on" suhete jaoks.
Kui teil on selge, hierarhiline taksonoomia, on `class` pärilus kõige loetavam ja tavapärasem lähenemine. `Juht` on `Töötaja`. `Säästukonto` on `Pangakonto`. See muster on hästi mõistetav ja kasutab kõige kaasaegsemat JavaScripti süntaksit.
-
Eelistage kompositsiooni keerukate objektide jaoks, millel on palju võimekusi.
Kui objektil peab olema mitu, sõltumatut ja vahetatavat käitumist, on kompositsioon parem. See hoiab ära sügava pesastumise ja loob paindlikuma, lahtisidestatud koodi. Mõelge kasutajaliidese komponendi ehitamisele, mis vajab selliseid funktsioone nagu lohistatavus, suuruse muutmise võimalus ja kokkupandavus. Need on paremad komponeeritud käitumistena kui sügava pärilusahelana.
-
Kasutage mixin'e ĂĽhise utiliitide komplekti jagamiseks.
Kui teil on läbivaid huvisid (cross-cutting concerns) – funktsionaalsus, mis kehtib paljude erinevat tüüpi objektide kohta (nagu logimine, silumine või andmete serialiseerimine) – on mixin'id suurepärane viis selle käitumise lisamiseks ilma peamist päriluspuud segamini ajamata.
-
Mõistke prototüübilist pärilust kui oma alustala.
Sõltumata sellest, millist kõrgetasemelist mustrit te kasutate, pidage meeles, et kõik JavaScriptis taandub prototüübiahelale. Selle aluse mõistmine annab teile võime siluda keerulisi probleeme ja tõeliselt vallata keele objektimudelit.
Kokkuvõte: JavaScripti OOP arenev maastik
JavaScripti lähenemine objektorienteeritud programmeerimisele peegeldab otseselt selle keele arengut. See algas lihtsa, võimsa ja mõnikord valesti mõistetud prototüübilise süsteemiga. Aja jooksul ehitasid arendajad selle süsteemi peale mustreid, et jäljendada klassikalist pärilust. Tänapäeval on meil ES6 klassidega puhas, kaasaegne süntaks, mis muudab OOP-i kättesaadavamaks, jäädes samal ajal truuks oma prototüübilistele juurtele.
Kuna kaasaegne tarkvaraarendus üle maailma liigub paindlikumate ja modulaarsemate arhitektuuride suunas, on mustrid nagu kompositsioon ja mixin'id esile tõusnud. Nad pakuvad võimsat alternatiivi jäikusele, mis võib mõnikord kaasneda sügavate pärilushierarhiatega. Oskuslik JavaScripti arendaja ei vali ainult ühte mustrit; ta mõistab kogu tööriistakasti. Ta teab, millal selge klassihierarhia on õige valik, millal koostada objekte väiksematest osadest ja kuidas aluseks olev prototüübiahel selle kõik võimalikuks teeb. Nende mustrite valdamisega saate kirjutada robustsemat, hooldatavamat ja elegantsemat koodi, olenemata sellest, milliseid väljakutseid teie järgmine projekt toob.